/*
 * Copyright (c) 2010-2013 Ruben Chakhmakhchyan. All rights reserved.
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Eclipse Public License
 * Version 1.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://opensource.org/licenses/EPL-1.0 and read it before using this file.
 */

#include "plugin_model_impl_common.h"


bool _PluginModel::m_static = false;
uint_t _PluginModel::m_osMajorVersion = 0;
void zoomCallback( AsyncInvokeArg* arg );

#define EPSILONF 0.001F
#define EPSILOND ( ( double ) EPSILONF )

char g_applicationName[] = APP_PRODUCTNAME;
char g_version[] = APP_PRODUCTVERSION;


PluginModel* CreatePluginModel( Plugin* pPlugin, MessageListener* messageListener )
{
    return new _PluginModel( pPlugin, messageListener );
}

_PluginModel::_PluginModel( Plugin* pPlugin, MessageListener* messageListener )
    : m_pPlugin( NULL ),
    m_messageListener( messageListener ),
    m_refresh( false ),
    m_hdc( NULL ),
    m_hRenderBitmap( NULL ),
    m_devicesRefreshed( false ),
    m_iaModel( NULL ),
    m_driverModelType( DriverModelType::Default ),
    m_transferMode( TransferMode::Default ),
    m_zoomState( false ), m_zoomFactor( 0.0F ), m_zoomFit( true ), m_fitZoom( 0.0F ),
    m_panState( false ), m_mousePan( false ),
    m_cropState( false ), m_mouseCrop( false ), m_mouseRectInitialized( false ),
    m_mouseZoom( false ), m_mouseZooming( false ),
    m_cropRectDefined( false ), m_mouseCropDragging( false ), m_mouseCropRefine( false ), m_cropSide( 0 ), m_cropCorner( 0 ),
    m_snapping( true ),
    m_showDeviceUI( false ),
    m_activeImage( NULL ),
    m_deviceIndex( 0 ),
    m_connected( false ),
    m_imageQuality( 50 ),
    m_renderDib( NULL ), m_renderDibSize( 0 ),
    m_autoPosition( true ),
    m_autoCompress( true ), m_backgroundCompression( true ), m_autoCompressionLevel( 5 ),
    m_cancelScan( 0 ), m_pauseScan( 0 ), m_autoPause( false ),
    m_hScanEvent( NULL ), m_hCompressEvent( NULL ), m_hImagesLockEvent( NULL ),
    m_imagesLocked( false ), m_imagesLockWaiting( false ),
    m_compressionComplete( 0 ),
    m_storeFormat( StoreFormat::Zlib ),
    m_thumbnailSize( 128 ),
    m_pSaveThreadHandle( NULL ),
    m_spacePressed( false ),
    m_updateBoundsResetPosition( true ),
    m_inverseZoomFactor( 0 ),
    m_numberOfImagesScanned( 0 ),
    m_activeImageIndex( -1 )
{
    if ( !m_static )
    {
        OSVERSIONINFOA versionInformation = { 0 };
        ::memset( &versionInformation, 0, sizeof( versionInformation ) );
        versionInformation.dwOSVersionInfoSize = sizeof( versionInformation );

        if ( !::GetVersionExA( &versionInformation ) )
        {
            return;
        }

        m_osMajorVersion = versionInformation.dwMajorVersion;

        m_static = true;
    }

    m_pPlugin = pPlugin;

    pPlugin->setModel( this );

    applyDriverModel();

    m_images.reserve( 64 );

    // initialize PDF filters
    m_pdfFilters[ 0 ] = ImageFilter::CCITT;
    m_pdfFilters[ 1 ] = ImageFilter::Flate;
    m_pdfFilters[ 2 ] = ImageFilter::DCT;

    // create scan event
    if ( !( m_hScanEvent = ::CreateEvent( NULL, TRUE, FALSE, NULL ) ) ) throw "Cannot create scan event!";

    // create images lock event
    if ( !( m_hImagesLockEvent = ::CreateEvent( NULL, TRUE, FALSE, NULL ) ) ) throw "Cannot create image list lock event!";

    // create compress event
    if ( !( m_hCompressEvent = ::CreateEvent( NULL, TRUE, FALSE, NULL ) ) ) throw "Cannot create compress event!";

    // test
    /*int* indices = NULL;
    int indexCount = 0;
    parseIndices( "9-17,52,57-61:even", indices, indexCount );
    int j = 0;*/

    //URLDESCRIPTOR urlDesc = { 0 };
    //Url::merge( urlDesc, NULL, "http://localhost/web/default2.aspx" );//"default.aspx?id=1"
}

HWND _PluginModel::getMessageWindow()
{
    return ( HWND ) this->getPlugin()->getWindowHandle();
}

_PluginModel::~_PluginModel()
{
    ::CloseHandle( m_hScanEvent );
    ::CloseHandle( m_hImagesLockEvent );
    ::CloseHandle( m_hCompressEvent );

    if ( m_activeImage ) m_activeImage->deactivate();
    m_activeImage = NULL;

    //requestUpdateView();

    // clear images
    m_images.destroy();

    if ( m_iaModel ) delete m_iaModel;

    // clear the render buffer
    if ( m_hRenderBitmap ) ::DeleteObject( m_hRenderBitmap );
    delete m_renderDib;
}

ImageAcquisitionModel* _PluginModel::getIaModel()
{
    return m_iaModel;
}

bool _PluginModel::setDriverModel( int value )
{
    m_driverModelType = ( DriverModelType ) value;

    return applyDriverModel();
}

bool _PluginModel::applyDriverModel()
{
    if ( m_iaModel && ( m_iaModel->getModelTypeId() != ( int ) m_driverModelType ) )
    {
        delete m_iaModel;
        m_iaModel = NULL;
    }

    if ( !m_iaModel )
    {
        if ( ( m_iaModel = ::CreateImageAcquisitionModel( this, this, ( int ) m_driverModelType ) ) != NULL )
        {
            m_iaModel->setParentWindow( this->m_pPlugin->getWindowHandle() );
            //m_iaModel->setIdentity( APP_PRODUCTNAME );
        }
    }

    return true;
}

bool _PluginModel::getDevices()
{
    CRITICAL_SECTION section = { 0 };

    ::InitializeCriticalSection( &section );
    ::EnterCriticalSection( &section );

    ImageAcquisitionModel* iam = getIaModel();

    if ( iam != NULL )
    {
        iam->getDevices();
    }

    m_devicesRefreshed = true;

    ::LeaveCriticalSection( &section );

    return true;
}

ImagingDevice* _PluginModel::getDevice( int index )
{
    if ( !m_iaModel )
        return false;

    return m_iaModel->getDevice( index );
}

int _PluginModel::getDeviceCount()
{
    if ( !m_iaModel ) return 0;
    return m_iaModel->getDeviceCount();
}

ResultCode _PluginModel::scan( bool add, int& numberOfImages )
{
    ::InterlockedExchange( &m_numberOfImagesScanned, 0 );

    ImageAcquisitionModel* iam = getIaModel();

    if ( iam == NULL ) return ResultCode::GeneralFailure;

    iam->setDeviceIndex( m_deviceIndex );

    ResultCode resultCode = iam->scan();

    // set the compression complete flag
    ::InterlockedExchange( &m_compressionComplete, 1 );

    numberOfImages = m_numberOfImagesScanned;

    return resultCode;
}

void _PluginModel::setImage( Pixels& pImage, bool add )
{
    if ( m_activeImage && m_autoCompress )
    {
        m_activeImage->deactivate(); // deactivate image so that it can be packed
        m_activeImage->lighten();
        m_activeImage = NULL;
    }

    Image* image = new Image( this, pImage );

    if ( add )
    {
        m_activeImageIndex = getImageCount();

        m_compressionQueue.push_back( m_activeImageIndex );

        m_images.push_back( image );
    }
    else
    {
        Image*& activeImage = m_images[ m_activeImageIndex ];

        delete activeImage;

        activeImage = image;
    }

    image->setIndex( m_activeImageIndex );

    image->lock();

    // assign the current image
    m_activeImage = image;
    m_activeImage->activate(); // activate image to block packing

    // store the image in compression is enabled
    if ( m_autoCompress )
    {
        if ( m_backgroundCompression )
        {
            if ( !::SetEvent( m_hCompressEvent ) )
                throw "Cannot set the compress event!";
        }
        else
        {
            m_activeImage->store();
        }
    }

    m_zoomFit = true;

    m_updateBoundsResetPosition = true;

    setViewport();

    m_refresh = true;

    if ( !add )
    {
        m_pPlugin->onImagePropertiesChanged( m_activeImageIndex );
    }

    //m_updateBoundsResetPosition = m_updateBounds = true;
}

void _PluginModel::selectImage( int index, bool refreshIfActive )
{
    if ( index < getImageCount() && ( refreshIfActive || index != m_activeImageIndex ) )
    {
        if ( m_activeImage && m_autoCompress )
        {
            m_activeImage->deactivate();
            m_activeImage->lighten();
            m_activeImage = NULL;
        }

        m_activeImage = m_images[ index ];

        m_activeImage->lock(); // keep the image locked until it's rendered

        m_activeImage->activate();
        m_activeImage->extract();

        m_activeImageIndex = index;

        m_zoomFit = true;

        m_refresh = true;

        m_updateBoundsResetPosition = true;

        setViewport();

        requestUpdateView();
    }
}

void _PluginModel::selectNextImage()
{
    selectImage( m_activeImageIndex );
}

void _PluginModel::selectPreviousImage()
{
    selectImage( m_activeImageIndex );
}

int _PluginModel::getDeviceIndex()
{
    return m_deviceIndex;
}

void _PluginModel::setDeviceIndex( int index )
{
    if ( index >= 0 )
        m_deviceIndex = index;
}

ImageFilter _PluginModel::getFilter( DocumentFormat format, PixelType pixelType ) const
{
    int index = -1;

    switch ( pixelType )
    {
    case PixelType::Bw: index = 0; break;
    case PixelType::Gray: index = 1; break;
    case PixelType::Rgb: index = 2; break;
    default: return ImageFilter::None;
    }

    return m_pdfFilters[ index ];
}

ImageFilter _PluginModel::getFilter( DocumentFormat format, uint_t bitsPerPixel ) const
{
    int index = -1;

    switch ( bitsPerPixel )
    {
    case 1: index = 0; break;
    case 8: index = 1; break;
    case 24: index = 2; break;
    default: return ImageFilter::None;
    }

    return m_pdfFilters[ index ];
}

void _PluginModel::setFilter( DocumentFormat format, PixelType pixelType, ImageFilter filter )
{
    if ( filter == ImageFilter::CCITT && pixelType != PixelType::Bw ) return;

    int index = -1;

    switch ( pixelType )
    {
    case PixelType::Bw: index = 0; break;
    case PixelType::Gray: index = 1; break;
    case PixelType::Rgb: index = 2; break;
    default: return;
    }

    m_pdfFilters[ index ] = filter;
}

ImageList* _PluginModel::getImageList( DocumentFormat format, int indices[], int count )
{
    ImageList* list = NULL;
    int totalImagesCount = m_images.count();

    if (  totalImagesCount == 0 ) return NULL;

    if ( format == DocumentFormat::Pdf )
    {
        byte_t* data = NULL;
        size_t size = 0;

        size = this->saveToStream( ( void** ) &data, format, indices, count );

        list->addItem( data, size, false );
    }
    else
    {
        list = new ImageList();

        int imageCount = indices ? count : totalImagesCount;

        for ( int i = 0; i < imageCount; i++ )
        {
            int index = i;

            if ( indices )
            {
                index = indices[ i ] - 1;
                if ( index < 0 || index >= totalImagesCount )
                    continue;
            }

            Image* image = m_images[ index ];

            byte_t* data = NULL;
            uint_t size = 0;

            if ( image->getStoredData() && image->getStoredDataFormat() == StoreFormat::Jpeg && format == DocumentFormat::Jpeg  )
            {
                data = ( byte_t* ) image->getStoredData();
                size = image->getStoredDataLength();

                list->addItem( data, size, true );
            }
            else
            {
                size = this->saveToStream( ( void** ) &data, format, indices ? indices + i : &( ++index ), 1 );

                list->addItem( data, size, false );
            }
        }
    }

    return list;
}

void _PluginModel::updateBounds()
{
    m_viewSize.height = ::lround( m_imageSize.height * m_zoomFactor, ( double ) 0.001F );
    m_viewSize.width = ::lround( m_imageSize.width * m_zoomFactor, ( double ) 0.001F );

    m_viewPortionRect.height = ( m_viewportSize.height > m_viewSize.height ) ? m_viewSize.height : m_viewportSize.height;
    m_viewPortionRect.width = ( m_viewportSize.width > m_viewSize.width ) ? m_viewSize.width : m_viewportSize.width;

    if ( m_updateBoundsResetPosition )
    {
        m_viewPortionRect.left = 0;
        m_viewPortionRect.top = 0;
    }

    if ( m_viewPortionRect.left + m_viewPortionRect.width > m_viewSize.width ) m_viewPortionRect.left = m_viewSize.width - m_viewPortionRect.width;
    if ( m_viewPortionRect.top + m_viewPortionRect.height > m_viewSize.height ) m_viewPortionRect.top = m_viewSize.height - m_viewPortionRect.height;

    m_viewPortionPos.x = ( m_viewportSize.width > m_viewSize.width ) ? ( m_viewportSize.width - m_viewSize.width ) >> 1 : 0;
    m_viewPortionPos.y = ( m_viewportSize.height > m_viewSize.height ) ? ( m_viewportSize.height - m_viewSize.height ) >> 1 : 0;

    if ( m_cropState )
    {
        m_cropRectViewPos.x = ::lround( m_cropRect.left * m_zoomFactor );
        m_cropRectViewPos.y = ::lround( m_cropRect.top * m_zoomFactor );
        m_cropRectSize.width = ::lround( m_cropRect.width * m_zoomFactor );
        m_cropRectSize.height = ::lround( m_cropRect.height * m_zoomFactor );

        m_cropRectWindowPos.x = m_cropRectViewPos.x - m_viewPortionRect.left + m_viewPortionPos.x;
        m_cropRectWindowPos.y = m_cropRectViewPos.y - m_viewPortionRect.top + m_viewPortionPos.y;
    }

    m_updateBoundsResetPosition = false;
}

bool _PluginModel::beginScan( bool add, AsyncCallback pAsyncCallback )
{
    PluginAsyncInvoke* asyncInvoke = new PluginAsyncInvoke();

    ScanAsyncArg* arg = new ScanAsyncArg( this, add );

    asyncInvoke->function = ::doScan;
    asyncInvoke->arg = arg;
    asyncInvoke->handle = getMessageWindow();
    asyncInvoke->callback = pAsyncCallback;

    // begin the scan thread
    m_pScanThreadHandle = ::beginInvoke( asyncInvoke );

    // start the background compression
    if ( m_autoCompress && m_backgroundCompression && m_paperSource == PaperSource::Feeder )
    {
        asyncInvoke = new PluginAsyncInvoke();

        arg = new ScanAsyncArg( this, add );

        asyncInvoke->function = ::doCompression;
        asyncInvoke->arg = arg;
        asyncInvoke->handle = NULL;
        asyncInvoke->callback = NULL;

        m_compressionComplete = 0;

        // begin the scan thread
        /*m_pCompressThreadHandle = */::beginInvoke( asyncInvoke );
    }

    return true;
}

void doScan( void* arg )
{
    ScanAsyncArg* a = ( ScanAsyncArg* ) arg;
    _PluginModel* model = ( _PluginModel* ) a->getPluginModel();

    int numberOfImages = 0;

    model->scan( a->getAdd(), numberOfImages );

    a->setResult( numberOfImages );
}

void doCompression( void* arg )
{
    ScanAsyncArg* a = ( ScanAsyncArg* ) arg;
    _PluginModel* model = ( _PluginModel* ) a->getPluginModel();

    // wait for the signal if in async mode

    bool proceed = true;
    HANDLE event = ( HANDLE ) ( model )->getCompressEvent();

    while ( proceed )
    {
        DWORD waitResult = ::WaitForSingleObject( event, 100L );

        switch ( waitResult )
        {
        case WAIT_OBJECT_0:
            {
                //unsigned int imageIndex = model->getCompressionIndex();
                uint_t imageIndex = -1;
                List<uint_t>& queue = model->getCompressionQueue();
                if ( queue.count() )
                {
                    imageIndex = queue.back();
                    queue.pop_back();

                    model->compressImage( imageIndex );
                }

                if ( queue.count() == 0 && model->getCompressionComplete() )
                {
#ifdef _DEBUG
                    ::printf( "compression complete: %i\n", imageIndex );
#endif

                    proceed = false;
                }

                ::ResetEvent( event );
            }
            break;

        case WAIT_TIMEOUT:
            {
                List<uint_t>& queue = model->getCompressionQueue();

                while ( queue.count() > 0 )
                {
                    uint_t imageIndex = queue.back();
                    queue.pop_back();

                    model->compressImage( imageIndex );
                }

                if ( model->getCompressionComplete() )
                {
#ifdef _DEBUG
                    printf( "compression complete (timeout)\n" );
#endif

                    proceed = false;
                }
            }
            break;
        }
    }
}

void _PluginModel::resize( size_t width, size_t height )
{
    m_viewportSize.width = width;
    m_viewportSize.height = height;

    setViewport();
}

void _PluginModel::setViewport()
{
    if ( !m_activeImage ) return;

    m_imageSize.width = m_activeImage->getWidth();
    m_imageSize.height = m_activeImage->getHeight();

    if ( m_viewportSize.height / ( double ) m_viewportSize.width < m_imageSize.height / ( double ) m_imageSize.width )
    {
        m_fitZoom = m_viewportSize.height / ( double ) m_imageSize.height;
    }
    else
    {
        m_fitZoom = m_viewportSize.width / ( double ) m_imageSize.width;
    }

    if ( m_zoomFit || m_fitZoom > m_zoomFactor )
    {
        m_updateBoundsResetPosition = true;

        setZoomFactor( m_fitZoom );
    }
    else
    {
        updateBounds();
    }

    m_refresh = true;
}

bool _PluginModel::stringToDocumentFormat( const wchar_t* string, DocumentFormat& format )
{
    if ( String::Equal( string, L"jpg" ) || String::Equal( string, L"jpeg" ) ) { format = DocumentFormat::Jpeg; }
    else if ( String::Equal( string, L"png" ) ) { format = DocumentFormat::Png; }
    else if ( String::Equal( string, L"gif" ) ) { format = DocumentFormat::Gif; }
    else if ( String::Equal( string, L"tiff" ) ) { format = DocumentFormat::Tiff; }
    else if ( String::Equal( string, L"bmp" ) ) { format = DocumentFormat::Bmp; }
    else if ( String::Equal( string, L"pdf" ) ) { format = DocumentFormat::Pdf; }
    else
        return false;

    return true;
}

bool _PluginModel::stringToDocumentFormat( const char* string, DocumentFormat& format )
{
    if ( String::Equal( string, "jpg" ) || String::Equal( string, "jpeg" ) ) { format = DocumentFormat::Jpeg; }
    else if ( String::Equal( string, "png" ) ) { format = DocumentFormat::Png;	}
    else if ( String::Equal( string, "gif" ) ) { format = DocumentFormat::Gif;	}
    else if ( String::Equal( string, "tiff" ) ) { format = DocumentFormat::Tiff; }
    else if ( String::Equal( string, "bmp" ) ) { format = DocumentFormat::Bmp;	}
    else if ( String::Equal( string, "pdf" ) ) { format = DocumentFormat::Pdf;	}
    else
        return false;

    return true;
}

void _PluginModel::setZoomFactor( double factor )
{
    if ( factor < m_fitZoom )
    {
        m_zoomFactor = m_fitZoom;
        m_zoomFit = true;
    }
    else
    {
        m_zoomFit = false;
        m_zoomFactor = factor > 16.0F ? 16.0F : factor; // max value: 1600%
    }

    m_inverseZoomFactor = 1.0f / m_zoomFactor;

    updateBounds();

    // invoke zoom-changed event

    AsyncInvokeArg* arg = new AsyncInvokeArg( this );
    postCallback( arg, ::zoomCallback );
}

void zoomCallback( AsyncInvokeArg* arg )
{
    // zoom changed event
    arg->getPluginModel()->getPlugin()->onZoomChanged();
}

void _PluginModel::setZoom( double factor )
{
    setZoomFactor( factor );

    m_refresh = true;

    requestUpdateView();
}

void _PluginModel::resetZoom()
{
    setZoom( 0.0F );
}

void _PluginModel::mouseLeftDown( short xpos, short ypos )
{
    if ( m_panState )
    {
        startMousePan( xpos, ypos );
    }
    else if ( m_cropState )
    {
        startMouseCrop( xpos, ypos );
    }
    else if ( m_zoomState )
    {
        startMouseZoom( xpos, ypos );
    }
}

void _PluginModel::mouseLeftUp( short xpos, short ypos )
{
    if ( m_panState )
    {
        stopMousePan();
    }
    else if ( m_cropState )
    {
        stopMouseCrop( xpos, ypos );
    }
    else if ( m_zoomState )
    {
        stopMouseZoom( xpos, ypos );
    }
}

void _PluginModel::mouseMove( short xpos, short ypos )
{
    if ( m_panState || m_spacePressed )
    {
        if ( m_spacePressed && !m_mousePan )
        {
            startMousePan( xpos, ypos );
        }
        doMousePan( xpos, ypos );
    }
    else if ( m_cropState )
    {
        doMouseCrop( xpos, ypos );
    }
    else if ( m_zoomState )
    {
        doMouseZoom( xpos, ypos );
    }
}

bool _PluginModel::keyDown( uint_t key )
{
    switch ( key )
    {
    case VK_SPACE:
        if ( !m_spacePressed )
        {
            m_spacePressed = true;
            return true;
        }
        break;
    }
    return false;
}

bool _PluginModel::keyUp( uint_t key )
{
    switch ( key )
    {
    case VK_SPACE:
        if ( m_spacePressed )
        {
            m_spacePressed = false;
            stopMousePan();
            return true;
        }
        break;
    }
    return false;
}

bool _PluginModel::pointInRect( Rect& r, int x, int y, int threshold )
{
    return x > r.left + threshold && x < r.getRight() - threshold && y > r.top + threshold && y < r.getBottom() - threshold;
}

int _PluginModel::pointOnSide( Rect& r, int x, int y, int threshold )
{
    int left = r.left, top = r.top, right = r.getRight(), bottom = r.getBottom();
    int leftt = left + threshold, topt = top + threshold, rightt = right - threshold, bottomt = bottom - threshold;

    if ( ::abs( y - top ) < threshold && x > leftt && x < rightt ) return 1;
    if ( ::abs( x - right ) < threshold && y > topt && y < bottomt ) return 2;
    if ( ::abs( y - bottom ) < threshold && x > leftt && x < rightt ) return 3;
    if ( ::abs( x - left ) < threshold && y > topt && y < bottomt ) return 4;

    return 0;
}

int _PluginModel::pointInCorner( Rect& r, int x, int y, int threshold )
{
    int left = r.left, top = r.top, right = r.getRight(), bottom = r.getBottom();
    bool aleft = ::abs( x - left ) < threshold, aright = ::abs( x - right ) < threshold, 
        atop = ::abs( y - top ) < threshold, abottom = ::abs( y - bottom ) < threshold;

    if ( aleft && atop ) return 1;
    if ( aright && atop ) return 2;
    if ( aright && abottom ) return 3;
    if ( aleft && abottom ) return 4;

    return 0;
}

void _PluginModel::postCallback( AsyncInvokeArg* arg, AsyncCallback callback )
{
    PluginAsyncCallback* asyncCallback = new PluginAsyncCallback();

    asyncCallback->arg = arg;
    asyncCallback->handle = this->getMessageWindow();
    asyncCallback->callback = callback;

    if ( !PostMessage( ( HWND ) asyncCallback->handle, WM_CUSTOM_ASYNCCALLBACK, NULL, ( LPARAM ) asyncCallback ) )
    {
        return;
    }
}

void _PluginModel::getViewSize( int& width, int& height )
{
    width = m_viewportSize.width;
    height = m_viewportSize.height;
}

void _PluginModel::setViewSize( const int width, const int height )
{
    m_viewportSize.width = width;
    m_viewportSize.height = height;
}

void imageReadyCallback( AsyncInvokeArg* arg )
{
    ( ( _PluginModel* ) arg->getPluginModel() )->imageReadyCallback( ( ( ImageAsyncArg* ) arg )->getImage() );
}

void _PluginModel::imageReadyCallback( Pixels& image )
{
    setImage( image, true );

    ::InterlockedIncrement( &m_numberOfImagesScanned );

    int imageCount = getImageCount();

    // image acquired event
    m_pPlugin->onImageAcquired( imageCount );

    // image count changed event
    m_pPlugin->onImageCountChanged( imageCount );

    // if the auto position property is set to true, update the current position
    if ( getAutoPosition() )
    {
        m_pPlugin->onSelectedImageChanged( imageCount );
    }

    // update the view
    requestUpdateView();
}

bool _PluginModel::connect()
{
    if ( !m_iaModel ) return false;

    m_iaModel->setDeviceIndex( m_deviceIndex );

    return m_iaModel->connect();
}

void _PluginModel::disconnect()
{
    if ( m_iaModel )
    {
        m_iaModel->disconnect();
    }
}

void _PluginModel::resumeScan( bool proceed )
{
    if ( m_autoPause && m_paperSource == PaperSource::Feeder )
    {
        ::InterlockedExchange( &m_cancelScan, proceed ? 0 : 1 );

        if ( !::SetEvent( m_hScanEvent ) )
            throw "Cannot set the scan event!";
    }
}

void _PluginModel::pauseScan()
{
    ::InterlockedExchange( &m_pauseScan, 1 );
}

void _PluginModel::stopScan()
{
    ::InterlockedExchange( &m_cancelScan, 1 );

    if ( m_autoPause && m_paperSource == PaperSource::Feeder )
    {
        if ( !::SetEvent( m_hScanEvent ) )
            throw "Cannot set the scan event!";
    }
}

const char* _PluginModel::getImageThumbnailUrl( int index ) const
{
    return m_images[ index ]->getThumbnailUrl();
}

const char* _PluginModel::getVersion() const
{
    return g_version;
}

void _PluginModel::clearImages()
{
    if ( m_activeImage ) m_activeImage->deactivate();
    m_activeImage = NULL;

    requestUpdateView();

    m_images.destroy();
    m_images.clear( m_images.count() > 1024 );

    m_pPlugin->onImageCountChanged( 0 );
    m_pPlugin->onSelectedImageChanged( 0 );
}

void _PluginModel::compressImage( uint_t index )
{
    m_images[ index ]->compress();
}

void _PluginModel::initializeDevice()
{
    if ( this->m_iaModel && this->m_deviceIndex )
    {
        this->m_iaModel->initializeDevice();
    }
    if ( m_iaModel->getIsPaperSourceFixed() )
    {
        m_paperSource = m_iaModel->getPaperSource();
    }
}

void _PluginModel::setPaperSource( PaperSource value )
{
    if ( m_iaModel && !m_iaModel->getIsPaperSourceFixed() )
    {
        m_paperSource = value;
    }
}

bool _PluginModel::getDuplex()
{
    if ( m_iaModel )
    {
        return m_iaModel->getDuplex();
    }
    return false;
}

void _PluginModel::setDuplex( bool value )
{
    if ( m_iaModel )
    {
        m_iaModel->setDuplex( value );
    }
}

PixelType _PluginModel::getPixelType() const
{
    return m_iaModel ? m_iaModel->getPixelType() : PixelType::Default;
}

void _PluginModel::setPixelType( const PixelType value )
{
    if ( m_iaModel )
    {
        m_iaModel->setPixelType( value );
    }
}

int _PluginModel::getResolution() const
{
    return m_iaModel ? m_iaModel->getResolution() : -1;
}

void _PluginModel::setResolution( const int value )
{
    if ( m_iaModel )
    {
        m_iaModel->setResolution( value );
    }
}

int _PluginModel::getContrast() const
{
    return m_iaModel ? m_iaModel->getContrast() : 0;
}

void _PluginModel::setContrast( const int value )
{
    if ( m_iaModel && value >= -100 && value <= 100 )
    {
        m_iaModel->setContrast( value * 10 );
    }
}


// ImageSite interface implementation

bool _PluginModel::createPixels( void* rawImage, Pixels& p )
{
    BITMAPINFOHEADER* pbih = ( BITMAPINFOHEADER* ) rawImage;
    BITMAPINFOHEADER& bih = *pbih;

    uint_t stride = ( ( bih.biWidth * bih.biBitCount + 31 ) & ( ~31 ) ) >> 3;

    p.width = bih.biWidth;
    p.height = ::abs( bih.biHeight );
    bool dir = bih.biHeight > 0;

    p.bitsPerPixel = bih.biBitCount;

    switch ( bih.biBitCount )
    {
    case 1:
    case 8: p.channels = 1; break;
    case 24: p.channels = 3; break;
    default: throw "UNKNOWN BIT COUNT!"; break;
    }
    p.bitsPerChannel = p.bitsPerPixel / p.channels;

    p.paletteInverted = bih.biBitCount == 1 && ( ( ( RGBQUAD* ) ( ( byte_t* ) rawImage + sizeof( BITMAPINFOHEADER ) ) )->rgbGreen == 0x00 );

    p.resolution = ( uint_t ) ::lround( bih.biXPelsPerMeter * ( double ) 0.0254F );

    p.rowStride = ( ( bih.biWidth * bih.biBitCount + 7 ) & ( ~7 ) ) >> 3;

    p.length = p.rowStride * p.height;
    p.pixels = ( byte_t* ) ::malloc( p.length );
    //memset( p.pixels, 0, p.length );

    byte_t* dibBits = ( byte_t* ) Dib::GetBits( pbih );

    long i = dir ? p.height - 1 : 0;
    byte_t* pline = dir ? dibBits + i * stride : dibBits;
    byte_t* nline = p.pixels;
    int increment = dir ? -1 : 1;
    int dirStride = dir ? - ( int ) stride : ( int ) stride;

    while ( dir ? i >= 0 : i < p.height )
    {
        ::memcpy( nline, pline, p.rowStride );
        if ( p.paletteInverted ) for ( uint_t j = 0; j < p.rowStride; j++ ) nline[ j ] = ~nline[ j ]; // invert palette to match standard internal representation

        pline += dirStride;
        nline += p.rowStride;
        i += increment;
    }

    p.paletteInverted = false; // in standard representation black is 1

    return true;
}

PBITMAPINFOHEADER _PluginModel::createDIB( const Pixels& p )
{
    uint_t colors = 0;
    switch ( p.bitsPerPixel )
    {
        //case 24: break;
    case 8:	colors = sizeof( RGBQUAD ) << 8; break;
    case 1: colors = sizeof( RGBQUAD ) << 1; break;
    }

    uint_t destStride = ( ( p.width * p.bitsPerPixel + 31 ) & ( ~31 ) ) >> 3;

    uint_t sizeImage = destStride * p.height;
    uint_t dataSize = sizeof( BITMAPINFOHEADER ) + colors + sizeImage;
    byte_t* destData = ( byte_t* ) ::malloc( dataSize );
    ::memset( destData, 0, dataSize );
    byte_t* destBits = destData + sizeof( BITMAPINFOHEADER ) + colors;

    BITMAPINFOHEADER& bih = *( ( PBITMAPINFOHEADER ) destData );
    bih.biSize = sizeof( BITMAPINFOHEADER );
    bih.biWidth = p.width;
    bih.biHeight = -p.height;
    bih.biSizeImage = sizeImage;
    bih.biBitCount = p.bitsPerPixel;
    bih.biClrImportant = colors;
    bih.biClrUsed = 0;
    bih.biYPelsPerMeter = bih.biXPelsPerMeter = ( LONG ) ( p.resolution / 0.254F );
    bih.biPlanes = 1;
    bih.biCompression = BI_RGB;

    byte_t* palette = destData + sizeof( BITMAPINFOHEADER );
    byte_t* pp = palette;

    if ( p.bitsPerPixel == 8 )
    {
        for ( int i = 0; i < 256; i++ )
        {
            RGBQUAD rgb = { i, i, i, 0 };
            ::memcpy( pp, &rgb, sizeof( RGBQUAD ) );
            pp += sizeof( RGBQUAD );
        }
    }
    else if ( p.bitsPerPixel == 1 )
    {
        RGBQUAD rgb1 = { 0xFF, 0xFF, 0xFF, 0 };
        ::memcpy( pp, &rgb1, sizeof( RGBQUAD ) );
        RGBQUAD rgb2 = { 0, 0, 0, 0 };
        ::memcpy( pp + sizeof( RGBQUAD ), &rgb2, sizeof( RGBQUAD ) );
    }

    byte_t* nline = destBits;
    byte_t* pline = p.pixels;
    long i = 0;

    while ( i < p.height )
    {
        ::memcpy( nline, pline, p.rowStride );

        pline += p.rowStride;
        nline += destStride;
        i++;
    }

    return ( PBITMAPINFOHEADER ) destData;
}

bool _PluginModel::imageReady( Pixels& image ) // runs in scanning thread
{
    PluginAsyncCallback* asyncCallback = new PluginAsyncCallback();
    ImageAsyncArg* arg = new ImageAsyncArg( this, image );

    asyncCallback->arg = arg;
    asyncCallback->handle = getMessageWindow();
    asyncCallback->callback = ::imageReadyCallback;

    if ( !::PostMessage( ( HWND ) asyncCallback->handle, WM_CUSTOM_ASYNCCALLBACK, NULL, ( LPARAM ) asyncCallback ) )
    {
        return false;
    }

    if ( ( m_autoPause || m_pauseScan ) && m_paperSource == PaperSource::Feeder )
    {
        // wait for the signal if in async mode

        bool proceed = true;
        HANDLE event = ( HANDLE ) m_hScanEvent;

        while( proceed )
        {
            DWORD dwWaitResult = ::WaitForSingleObject( event, 10L );

            switch ( dwWaitResult )
            {
            case WAIT_OBJECT_0:

                proceed = false;
                ::ResetEvent( event );
                break;

            case WAIT_TIMEOUT: break;
            }
        }

        if ( m_pauseScan ) m_pauseScan = 0;
    }

    return true;
}

bool _PluginModel::deleteImage( int index )
{
    if ( !hasImage() || index < 0 || index >= m_images.count() ) return false;

    m_activeImage = NULL;

    bool isLast = index == m_images.count() - 1;

    int newIndex = isLast ? index - 1 : index;

    delete ( m_images[ index ] );

    m_images.removeAt( index );

    selectImage( newIndex, true );

    m_pPlugin->onImageCountChanged( getImageCount() );
    if ( isLast )
    {
        m_pPlugin->onSelectedImageChanged( m_activeImageIndex );
    }

    requestUpdateView();

    return true;
}

bool _PluginModel::shiftImage( int offset )
{
    if ( !hasImage() ) return false;
    if ( offset == 0 ) return true;

    int destIndex = m_activeImageIndex + offset;
    if ( destIndex < 0 || destIndex >= m_images.count() ) return false;

    m_activeImage = NULL;

    lockImages( true );

    m_images.shift( m_activeImageIndex, offset );

    //selectImage( destIndex, true );

    lockImages( false );

    selectImage( destIndex, true );
    m_pPlugin->onSelectedImageChanged( m_activeImageIndex );

    return false;
}

void _PluginModel::lockImages( bool lock )
{
    if ( m_hImagesLockEvent == NULL ) return;

    if ( lock )
    {
        if ( m_imagesLocked )
        {
#ifdef _DEBUG
            //printf( "wait image list lock: %i\n", m_index );
#endif

            bool proceed = true;
            HANDLE event = m_hImagesLockEvent;
            m_imagesLockWaiting = true;

            while( proceed )
            {
                DWORD waitResult = ::WaitForSingleObject( event, 50L );

                switch ( waitResult )
                {
                case WAIT_OBJECT_0:
                    {
                        proceed = false;
                        m_imagesLockWaiting = false;
                        ::ResetEvent( event );
                    }
                    break;

                case WAIT_TIMEOUT: break;
                }
            }
        }

#ifdef _DEBUG
        //printf( "images lock: %i\n", m_index );
#endif

        m_imagesLocked = true;
    }
    else
    {
        if ( m_imagesLocked )
        {
#ifdef _DEBUG
            //printf( "unlock: %i\n", m_index );
#endif
            if ( m_imagesLockWaiting )
            {
                if ( !::SetEvent( m_hImagesLockEvent ) )
                    throw "Cannot set the images lock event!";
            }
            m_imagesLocked = m_imagesLockWaiting = false;
        }
    }
}
